package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisData;
import com.hmdp.utils.SystemConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private CacheClient cacheClient;

    /**
     * 根据id查询商铺信息
     *
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        //缓存穿透
        //Shop shop = queryWithPassThrough(id)
        //Shop shop = cacheClient.queryWithPassThrough(SystemConstants.CACHE_SHOP, id, Shop.class, this::getById, SystemConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //互斥锁解决缓存击穿
        //Shop shop = queryWitMutex(id);
        //Shop shop = cacheClient.queryWitMutex(SystemConstants.CACHE_SHOP, SystemConstants.LOCK_SHOP_KEY, id, Shop.class, this::getById, SystemConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);


        //逻辑过期解决缓存击穿
        //(不考虑缓存穿透,因为逻辑过期是用于处理热点key数据的,这类数据通常会在事先主动将数据库中对应的所有数据缓存到redis中)
        //(因此如果查询到不存在缓存redis中的数据时,则数据库中也不存在相应的数据)
        Shop shop = queryWithLogicalExpire(id);
        //Shop shop = cacheClient.queryWithLogicalExpire(SystemConstants.CACHE_SHOP,id,Shop.class,this::getById,70L,TimeUnit.SECONDS);
        if (shop == null) {
            return Result.fail("店铺不存在");
        }
        //返回
        return Result.ok(shop);
    }

    //逻辑过期创建的新线程
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 缓存穿透解决方案(返回空值)
     *
     * @param id
     * @return
     */
    //缓存穿透解决方案(返回空值)
    public Shop queryWithPassThrough(Long id) {
        String key = SystemConstants.CACHE_SHOP + id;
        //从redis中查寻商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        //判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //判断命中的是否为空值
        if (shopJson.equals("")) {
            //返回错误信息
            return null;
        }
        //不存在,去数据库查询
        Shop shop = getById(id);

        if (shop == null) {
            //如果数据库不存在数据
            //将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", 2L, TimeUnit.MINUTES);
            return null;
        }

        //如果数据库存在数据,添加写入redis缓存,并返回
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), 30L + RandomUtil.randomInt(5, 20), TimeUnit.MINUTES);

        return shop;
    }


    /**
     * 缓存击穿解决方案(互斥锁)
     *
     * @param id
     * @return
     */
    //缓存击穿解决方案(互斥锁)
    public Shop queryWitMutex(Long id) {
        String key = SystemConstants.CACHE_SHOP + id;
        //从redis中查寻商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        //判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //判断命中的是否为空值
        if (shopJson != null) {
            //返回错误信息
            return null;
        }
        //不存在,实现缓存重建

        //获取互斥锁
        String lockkey = "lock:shop" + id;
        Shop shop = null;
        boolean isLock = false;

        try {
            while (!isLock) {
                try {
                    isLock = tryLock(lockkey);
                    if (isLock) {//如果获取锁成功直接跳出去查数据库
                        break;
                    }
                    Thread.sleep(50);

                    //从redis中查寻商铺缓存
                    shopJson = stringRedisTemplate.opsForValue().get(key);
                    //判断是否存在
                    if (StrUtil.isNotBlank(shopJson)) {
                        //存在,直接返回
                        return JSONUtil.toBean(shopJson, Shop.class);
                    }
                    //判断命中的是否为空值
                    if (shopJson != null) {
                        //返回错误信息
                        return null;
                    }
                    //不存在,实现缓存重建


                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }


            //成功,去数据库查询
            shop = getById(id);
            Thread.sleep(200);
            if (shop == null) {
                //如果数据库不存在数据
                //将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", 2L, TimeUnit.MINUTES);
                return null;
            }
            //如果数据库存在数据,添加写入redis缓存,并返回
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), 30L + RandomUtil.randomInt(5, 20), TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //释放互斥锁
            unlock(lockkey);
        }
        //返回
        return shop;
    }


    /**
     * 缓存击穿解决方案(逻辑过期)
     *
     * @param id
     * @return
     */
    //缓存击穿解决方案(逻辑过期)
    //(不考虑缓存穿透,因为逻辑过期是用于处理热点key数据的,这类数据通常会在事先主动将数据库中对应的所有数据缓存到redis中)
    //(因此如果查询到不存在缓存redis中的数据时,则数据库中也不存在相应的数据)
    //此时会将一个空的shop和逻辑过期时间组成的RedisData存入缓存,并且永久市场,防止被穿透.类似于生成了一个null在redis中
    public Shop queryWithLogicalExpire(Long id) {
        String key = SystemConstants.CACHE_SHOP + id;

        //从redis中查寻商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);

        //判断是否存在
        //判断缓存是否命中
        if (StrUtil.isBlank(shopJson)) {
            //未命中,返回空
            return null;
        }

        //命中,需要把json反序列化为RedisData对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        // 这里需要先转成JSONObject再转成反序列化，否则可能无法正确映射Shop的字段
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        //判断缓存是否过期
        LocalDateTime expireTime = redisData.getExpireTime();

        if (expireTime.isAfter(LocalDateTime.now())) {
            //如果没有过期,返回商铺信息
            return shop;
        }

        //如果过期,实现缓存重建
        //判断是否获得互斥锁互斥锁
        String lockKey = SystemConstants.LOCK_SHOP_KEY + id;
        //如果已经过期,尝试获取互斥锁(setnx实现)
        Boolean lock = tryLock(SystemConstants.LOCK_SHOP_KEY);
        if (lock) {
            /**
             * 当有一个线程获取锁成功后,其余的所有线程都是认为此时的缓存是过期的
             * 也就是说,如果没有第一个double chack 那么除了if (lock) {}里面的线程以外,其余代码中的线程皆认为当前缓存已经过期,需要重建
             * 而如果此时释放锁,在if (lock) {}上面的代码中(此时依旧认为缓存过期的)的线程就会获取锁成功,然后再次执行重建缓存代码,这一次的重建缓存是没有必要的
             *
             * 如果有第一次double chack 在if (lock) {}里面面的代码中(认为缓存过期的)的线程会进行缓存查询,二次判断是否缓存过期
             * 因为有可能现在能拿到锁是因为别人 已经更新完缓存  后 释放锁才让你拿到的,现在缓存都更新了,你却拿到了锁要去再更新一遍,就会造成资源浪费
             */
            //注意:获取锁成功后应该再次检测redis缓存是否过期,做DoubleCheck
            //如果存在则无需重建缓存
            //DoubleChack
            String shopJsonChack = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isBlank(shopJsonChack)) {
                //未命中,返回空
                return null;
            }
            // 3.2 缓存命中，将JSON字符串反序列化未对象，并判断缓存数据是否逻辑过期
            RedisData redisDataChack = JSONUtil.toBean(shopJsonChack, RedisData.class);
            // 这里需要先转成JSONObject再转成反序列化，否则可能无法正确映射Shop的字段
            Shop shopChack = JSONUtil.toBean((JSONObject) redisDataChack.getData(), Shop.class);
            LocalDateTime expireTimeChack = redisDataChack.getExpireTime();

            if (expireTimeChack.isAfter(LocalDateTime.now())) {
                // 当前缓存数据未过期，直接返回
                return shopChack;
            }

            //如果获取锁成功,过期数据直接返回,开启线程独立,查询数据库并将新数据添加入redis
            CACHE_REBUILD_EXECUTOR.submit(() -> {

                //重建缓存
                try {
                    this.saveShopToRedis(id, SystemConstants.LOCK_SHOP_TTL);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    //存入redis后释放锁
                    unlock(lockKey);
                }
            });
        }

        /**
         *
         * 如果有第二次double chack此时如果释放锁,那么上面的代码中(认为缓存过期的)的线程会查询到刚刚更新的最新缓存,而不会依旧返回旧数据,使数据一致性增强
         */
        //DoubleChack
        shopJson = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isBlank(shopJson)) {
            //未命中,返回空
            return null;
        }
        // 3.2 缓存命中，将JSON字符串反序列化未对象，并判断缓存数据是否逻辑过期
        redisData = JSONUtil.toBean(shopJson, RedisData.class);
        // 这里需要先转成JSONObject再转成反序列化，否则可能无法正确映射Shop的字段
        shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            // 当前缓存数据未过期，直接返回
            return shop;
        }
        //返回旧数据
        return shop;
    }


    /**
     * 逻辑过期
     * 将shop和逻辑过期时间封装入RedisData中并写入redis中
     *
     * @param id
     * @param expireSeconds
     */
    //逻辑过期
    //将shop和逻辑过期时间封装入RedisData中并写入redis中
    public void saveShopToRedis(Long id, Long expireSeconds) throws InterruptedException {
        String key = SystemConstants.CACHE_SHOP + id;
        //1.查询店铺数据
        Shop shop = getById(id);
        Thread.sleep(200);
        //2.封装逻辑过期时间
        RedisData redisData = RedisData.builder()
                .data(shop)
                .expireTime(LocalDateTime.now().plusSeconds(expireSeconds))
                .build();

        //3.写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 设置互斥锁(用redis的setnx实现)
     *
     * @param key
     * @return
     */
    //设置互斥锁(用redis的setnx实现)
    private Boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     *
     * @param key
     */
    //释放锁
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

    /**
     * 缓存和数据库同步更新
     *
     * @param shop
     * @return
     */
    @Transactional//由于执行删除redis缓存这一步可能有抛错,会影响修改数据库后与缓存数据不同步,
    // 所以加个事务注解,使redis删除抛错后连带数据库一起回滚
    @Override
    public Result update(Shop shop) {
        if (shop.getId() == null) {
            return Result.fail("商铺id不能为空");
        }
        //先修改数据库
        updateById(shop);
        //再删除redis
        stringRedisTemplate.delete(SystemConstants.CACHE_SHOP + shop.getId());
        return Result.ok();
    }
}
